/************************************************************************
* map.c
* voxelands - 3d voxel world sandbox game
* Copyright (C) Lisa 'darkrose' Milne 2016 <lisa@ltmnet.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
************************************************************************/

#include "common.h"
#include "map.h"
#include "list.h"

#include "content_block.h"

static struct {
	mapoct_t map;
	chunk_t *chunks;
	mutex_t *mut;
	chunk_t *last;
} map_data = {
	{
		65536,
		{0,0,0},
		NULL,
		0,
		{NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL},
		{NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL}
	},
	NULL,
	NULL,
	NULL
};

static void mapoct_delete(mapoct_t *m)
{
	if (m->size == 65536)
		return;
}

static int mapoct_index(mapoct_t *m, pos_t *p)
{
	int i = 0;

	if (p->x < m->pos.x) {
		if (p->y < m->pos.y) {
			if (p->z < m->pos.z) {
				i = MAPOCT_MINUS_XYZ;
			}else{
				i = MAPOCT_MINUS_XY_PLUS_Z;
			}
		}else if (p->z < m->pos.z) {
			i = MAPOCT_MINUS_XZ_PLUS_Y;
		}else{
			i = MAPOCT_MINUS_X_PLUS_YZ;
		}
	}else if (p->y < m->pos.y) {
		if (p->z < m->pos.z) {
			i = MAPOCT_MINUS_YZ_PLUS_X;
		}else{
			i = MAPOCT_MINUS_Y_PLUS_XZ;
		}
	}else if (p->z < m->pos.z) {
		i = MAPOCT_MINUS_Z_PLUS_XY;
	}else{
		i = MAPOCT_PLUS_XYZ;
	}

	return i;
}

static mapoct_t *mapoct_walk(mapoct_t *m, pos_t *p, uint8_t create);
static mapoct_t *mapoct_walk(mapoct_t *m, pos_t *p, uint8_t create)
{
	int i;

	if (m->size == (CHUNKSIZE*2))
		return m;

	i = mapoct_index(m,p);

	if (!m->children[i]) {
		mapoct_t *mm;
		int k;
		if (!create)
			return NULL;

		mm = malloc(sizeof(mapoct_t));
		if (!mm)
			return NULL;

		mm->parent = m;
		mm->parent_index = i;
		mm->size = m->size/2;

		k = mm->size/2;

		if (p->x < m->pos.x) {
			mm->pos.x = m->pos.x-k;
		}else{
			mm->pos.x = m->pos.x+k;
		}
		if (p->y < m->pos.y) {
			mm->pos.y = m->pos.y-k;
		}else{
			mm->pos.y = m->pos.y+k;
		}
		if (p->z < m->pos.z) {
			mm->pos.z = m->pos.z-k;
		}else{
			mm->pos.z = m->pos.z+k;
		}

		for (k=0; k<8; k++) {
			mm->children[k] = NULL;
			mm->chunks[k] = NULL;
		}

		m->children[i] = mm;
	}

	if (m->children[i]->size == (CHUNKSIZE*2))
		return m->children[i];

	return mapoct_walk(m->children[i],p,create);
}

static chunk_t *mapoct_walk_chunk(mapoct_t *m, pos_t *p)
{
	int i = 0;

	m = mapoct_walk(m,p,0);
	if (!m)
		return NULL;
	if (m->size != (CHUNKSIZE*2))
		return NULL;

	i = mapoct_index(m,p);

	return m->chunks[i];
}

/* intialise the map data */
int map_init()
{
	if (!map_data.mut)
		map_data.mut = mutex_create();

	return mapgen_init();
}

/* delete and free the current map */
void map_clear()
{
	while (map_data.chunks) {
		map_delete_chunk(map_data.chunks);
	}
}

/* deinitialise the map */
void map_exit()
{
	mapgen_exit();
	map_clear();
	map_data.last = NULL;
}

/* add a new chunk to the map */
void map_add_chunk(chunk_t *ch)
{
	int i;
	mapoct_t *m;

	mutex_lock(map_data.mut);

	m = mapoct_walk(&map_data.map,&ch->pos,1);

	if (!m)
		return;

	i = mapoct_index(m,&ch->pos);

	m->chunks[i] = ch;
	ch->mapoct = m;
	ch->mapoct_index = i;

	map_data.chunks = list_append(&map_data.chunks,ch);

	mutex_unlock(map_data.mut);

	map_trigger(MAP_TRIGGER_LOADED,ch,&ch->pos);
}

/* delete a chunk from the map */
void map_delete_chunk(chunk_t *ch)
{
	int i;
	mapoct_t *m;

	mutex_lock(map_data.mut);

	map_data.chunks = list_remove(&map_data.chunks,ch);

	m = ch->mapoct;
	m->chunks[ch->mapoct_index] = NULL;

	map_trigger(MAP_TRIGGER_UNLOAD,ch,&ch->pos);

	free(ch);

	for (i=0; i<8; i++) {
		if (m->chunks[i]) {
			mutex_unlock(map_data.mut);
			return;
		}
	}

	mapoct_delete(m);

	mutex_unlock(map_data.mut);
}

/* get the map chunk containing a position */
chunk_t *map_get_chunk_containing(pos_t *p)
{
	chunk_t *ch;

	if (map_data.last) {
		ch = map_data.last;
		if (
			p->x >= ch->pos.x && p->x < ch->pos.x+16
			&& p->y >= ch->pos.y && p->y < ch->pos.y+16
			&& p->z >= ch->pos.z && p->z < ch->pos.z+16
		)
			return ch;
	}

	mutex_lock(map_data.mut);

	ch = mapoct_walk_chunk(&map_data.map,p);

	mutex_unlock(map_data.mut);

	if (ch) {
		map_data.last = ch;
		return ch;
	}

	/* TODO: load from disk, possibly asyncronously */

	mapgen_request(p);

	return NULL;
}

/* get the map block at a position */
block_t *map_get_block(pos_t *p)
{
	chunk_t *ch;
	int x;
	int y;
	int z;

	ch = map_get_chunk_containing(p);
	if (!ch)
		return NULL;

	x = p->x%CHUNKSIZE;
	y = p->y%CHUNKSIZE;
	z = p->z%CHUNKSIZE;

	if (x<0)
		x += CHUNKSIZE;
	if (y<0)
		y += CHUNKSIZE;
	if (z<0)
		z += CHUNKSIZE;

	return &ch->blocks[x][y][z];
}

/* add/update a block */
void map_set_block(pos_t *p, block_t *b)
{
	chunk_t *ch;
	block_t *cb;
	int x;
	int y;
	int z;

	ch = map_get_chunk_containing(p);
	if (!ch)
		return;

	x = p->x%CHUNKSIZE;
	y = p->y%CHUNKSIZE;
	z = p->z%CHUNKSIZE;

	if (x<0)
		x += CHUNKSIZE;
	if (y<0)
		y += CHUNKSIZE;
	if (z<0)
		z += CHUNKSIZE;

	cb = &ch->blocks[x][y][z];

	if (b) {
		cb->content = b->content;
		cb->param1 = b->param1;
		cb->param2 = b->param2;
		cb->param3 = b->param3;
		cb->envticks = b->envticks;
	/* NULL is air */
	}else{
		cb->content = CONTENT_AIR;
		cb->param1 = 0;
		cb->param2 = 0;
		cb->param3 = 0;
		cb->envticks = 0;
	}

	map_trigger(MAP_TRIGGER_UPDATE,ch,&ch->pos);
}
